Skip to content

fix: warn when an asset is skipped at the package boundary - #607

Open
puneet-ekline wants to merge 1 commit into
vercel:mainfrom
puneet-ekline:fix/warn-on-package-boundary-asset-skip
Open

fix: warn when an asset is skipped at the package boundary#607
puneet-ekline wants to merge 1 commit into
vercel:mainfrom
puneet-ekline:fix/warn-on-package-boundary-asset-skip

Conversation

@puneet-ekline

@puneet-ekline puneet-ekline commented Aug 11, 2026

Copy link
Copy Markdown

Note

Used AI to investigate and propose this fix.

Closes #606 (partially — see "What this does not change" below).

Problem

The package-boundary guard added in #568 stops a package inside node_modules from emitting assets outside node_modules. The skip is currently only visible under job.log, so in a normal build the file is simply absent from the output and the first symptom is a runtime failure in the deployed application.

This cost us a multi-hour production outage. next-i18next resolves its user config from the current working directory:

// next-i18next/dist/commonjs/serverSideTranslations.js
const configPath = path.resolve('./next-i18next.config.js');
if (!userConfig && fs.existsSync(configPath)) {
  userConfig = await import(configPath);
}
if (userConfig === null) {
  throw new Error(`next-i18next was unable to find a user config at ${configPath}`);
}

Babel compiles that dynamic import() into a Promise wrapper around require, which is analysed as an asset rather than a dependency, so the path flows through emitAssetPath and hits the guard. After Next.js 16.3.0 picked up a post-1.3.2 nft, the config stopped reaching the serverless bundle, existsSync returned false, and next-i18next threw before any page rendered — 500ing every server-rendered response, including the on-demand 404. next build exited 0 throughout, and there was nothing in the output to indicate why.

Looking up a config file from process.cwd() is a widespread convention, so this likely reaches beyond next-i18next.

What this changes

The skip is added to warnings in addition to the existing job.log output, so consumers can surface it at build time.

const message =
  'Skipping asset emission of ' + assetPath + ' for ' + id +
  ' as it is outside the package base ' + pkgBase;
job.warnings.add(new Error(message));
if (job.log) console.log(message);

What this does not change

Behaviour is identical — the asset is still not emitted, and no existing expectation changes. I have deliberately not touched the boundary rule itself: the fixtures added in #568 assert it, so whether in-base assets should be skipped is a maintainer decision. That question is #606; this PR only makes the outcome observable either way.

New test fixture

test/unit/pkg-cwd-asset-outside-pkg-base covers a case the existing *-outside-base fixtures do not. Those use /../../secret.txt, which normalises to /secret.txt; that path does not exist, so tracing returns at the stat check before reaching this guard — they pass without exercising it. Verified by instrumenting the branch: neither fixture reaches it.

The new fixture uses the Babel-transpiled dynamic import of a cwd-resolved path, which does reach the guard, and asserts:

  • the asset is still absent from fileList (behaviour preserved)
  • a warning is emitted

The assertion is scoped to the uncached pass, since the second run replays cached analysis and does not re-emit warnings.

Verification

  • jest test/unit.test.js test/ecmascript.test.js: 1375 passed (1373 before, plus the two variants of the new fixture)
  • prettier --check clean on all touched files
  • Confirmed the new assertion is meaningful: reverting src/analyze.ts alone makes it fail
  • Confirmed against the real-world case — tracing next-i18next's serverSideTranslations entry now yields a warning naming next-i18next.config.js, where previously the file was dropped with no signal

ecmascript.test.js asserts warnings.size === 0, and it still passes, so no existing fixture triggers this path.

The package-boundary guard added in vercel#568 stops a package inside node_modules
from emitting assets outside node_modules. That is deliberate, but the skip is
currently only visible under `job.log`, so in a normal build the file is simply
absent from the output and the first symptom is a runtime failure in the
deployed application.

Surface it in `warnings` as well, so consumers can see and act on it at build
time. Behaviour is unchanged: the asset is still not emitted.

This cost us a multi-hour production outage (see vercel#606). next-i18next resolves
its user config from the current working directory:

    const configPath = path.resolve('./next-i18next.config.js');
    if (!userConfig && fs.existsSync(configPath)) {
      userConfig = await import(configPath);
    }

Babel compiles that dynamic import to a Promise wrapper around `require`, which
is analysed as an asset rather than a dependency, so the path goes through
emitAssetPath and is skipped. The config never reached the serverless bundle,
`existsSync` returned false, and next-i18next threw before any page rendered --
with nothing in the build output to indicate why.

The new fixture covers the case the existing *-outside-base fixtures do not:
their `/../../secret.txt` normalises to a path that does not exist, so it
returns at the stat check before reaching this guard.

Tests: 1375 passed (1373 before, plus the two variants of the new fixture).
@puneet-ekline
puneet-ekline requested review from a team, icyJoseph, ijjk and styfle as code owners August 11, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1.3.2 package-boundary guard silently drops cwd-anchored config files referenced by node_modules packages

1 participant